﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Caching;
using System.Web;
using System.Web.Caching;

namespace MvcSupplantCaching
{
    /// <summary>
    /// The <see cref="OutputCacheProvider"/> that wraps <see cref="MemoryCache.Default"/> for the purposes of using the default Output Cache.
    /// </summary>
    public class MemoryCacheProvider : BaseOutputCacheProvider, IEnumerable<KeyValuePair<string, object>>
    {
        private readonly IDictionary _fallbackCache = new Dictionary<string, object>();
        private static readonly ObjectCache Cache = MemoryCache.Default;

        internal MemoryCacheProvider() { }

        /// <summary>
        /// Adds an object to the output cache.
        /// </summary>
        /// <param name="key">The cache key to use for the object.</param>
        /// <param name="entry">The new cache entry to be added to the cache.</param>
        /// <param name="utcExpiry">The absolute time the entry expires from the cache.</param>
        /// <returns>The entry that was added to the cache.</returns>
        protected override object AddEntry(string key, object entry, DateTime utcExpiry)
        {
            var policy = new CacheItemPolicy();
            policy.AbsoluteExpiration = utcExpiry;
            policy.RemovedCallback = RemovedCallback;
            return Cache.AddOrGetExisting(key, entry, policy);    
        }

        /// <summary>
        /// Retrieves an object from the cache by a key.
        /// </summary>
        /// <param name="key">The cache key to get the entry for.</param>
        /// <returns>The cached object.</returns>
        protected override object GetEntry(string key)
        {
            return Cache.Get(key);
        }

        /// <summary>
        /// Removes an entry from the cache.
        /// </summary>
        /// <param name="key">The cache key to remove the entry for.</param>
        protected override void RemoveEntry(string key)
        {
            Cache.Remove(key);
        }

        /// <summary>
        /// Sets an entry in the cache, creating it if it did not exist before.
        /// </summary>
        /// <param name="key">The cache key to use for the object.</param>
        /// <param name="entry">The entry to be updated or created.</param>
        /// <param name="utcExpiry">The absolute time the entry expires from the cache.</param>
        protected override void SetEntry(string key, object entry, DateTime utcExpiry)
        {
            var policy = new CacheItemPolicy();
            policy.AbsoluteExpiration = utcExpiry;
            policy.RemovedCallback = RemovedCallback;
            Cache.Set(key, entry, policy);
        }

        /// <summary>
        /// Gets an enumerable of the entries in the cache.
        /// </summary>
        /// <returns>An IEnumerable of keys to entries.</returns>
        public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
        {
            return ((IEnumerable<KeyValuePair<string, object>>) Cache).GetEnumerator();
        }

        /// <summary>
        /// Returns the enumerator.
        /// </summary>
        /// <returns>The enumerator for the cache entries.</returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        /// <summary>
        /// The HttpContext.Current.Items IDictionary cache.
        /// </summary>
        protected override IDictionary ImplementedRequestCache
        {
            get { return HttpContext.Current != null ? HttpContext.Current.Items : _fallbackCache; }
        }

        /// <summary>
        /// Gets called when an item is removed from the Cache object.
        /// </summary>
        /// <param name="arguments">The arguments providing details on the item that was removed.</param>
        protected void RemovedCallback(CacheEntryRemovedArguments arguments)
        {
            var key = arguments.CacheItem.Key;

            // If the removed item is a cache dependency, remove the related item as well.
            var dependencyItem = arguments.CacheItem.Value as CacheDependencyItem;
            if (dependencyItem != null)
            {
                foreach (string outputCacheKey in dependencyItem.RelatedOutputCacheKeys)
                {
                    Cache.Remove(outputCacheKey);
                }
                return;
            }
            OutputCache.SupplantProvider.Remove(key);
        }
    }
}